{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "from d2l import torch as d2l"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "def corr2d(X, k):\n",
    "    \"\"\"二维互相关运算\"\"\"\n",
    "    kh, kw = k.shape\n",
    "    Y = torch.zeros((X.shape[0] - kh + 1),(X.shape[1] - kw + 1))\n",
    "    for h in range(Y.shape[0]):\n",
    "        for w in range(Y.shape[1]):\n",
    "            Y[h,w] = (X[h:h+kh, w:w+kw] * k).sum()\n",
    "    return Y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([2, 2])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "tensor([[19., 25.],\n",
       "        [37., 43.]])"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = torch.arange(9).reshape(3, 3)\n",
    "k = torch.arange(4).reshape(2, 2)\n",
    "Y = corr2d(X, k)\n",
    "Y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = torch.arange(9, dtype=torch.float32).reshape(3, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Conv2D(nn.Module):\n",
    "    \"\"\"实现二维卷积\"\"\"\n",
    "    def __init__(self, kernel_size):\n",
    "        super().__init__()\n",
    "        self.weight = nn.Parameter(torch.rand(kernel_size))\n",
    "        self.bias = nn.Parameter(torch.zeros(1))\n",
    "        \n",
    "    def forward(self, X):\n",
    "        return corr2d(X, self.weight) + self.bias"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = torch.ones(6,8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[1., 1., 0., 0., 0., 0., 1., 1.],\n",
       "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
       "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
       "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
       "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
       "        [1., 1., 0., 0., 0., 0., 1., 1.]])"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X[:, 2:6] = 0\n",
    "X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2])"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "k = torch.tensor([[1, -1]])\n",
    "k.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([6, 7])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
       "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
       "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
       "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
       "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
       "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Y = corr2d(X,k)\n",
    "Y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([8, 5])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "tensor([[0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0.]])"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "corr2d(X.t(), k)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0., 0., 0., 0.],\n",
       "        [0., 0., 0., 0., 0., 0., 0., 0.]])"
      ]
     },
     "execution_count": 50,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "corr2d(X, k.t())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = Conv2D((1, 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[1.0927, 0.3496, 0.0000, 0.0000, 0.0000, 0.7431, 1.0927, 1.0927],\n",
       "        [1.0927, 0.3496, 0.0000, 0.0000, 0.0000, 0.7431, 1.0927, 1.0927],\n",
       "        [1.0927, 0.3496, 0.0000, 0.0000, 0.0000, 0.7431, 1.0927, 1.0927],\n",
       "        [1.0927, 0.3496, 0.0000, 0.0000, 0.0000, 0.7431, 1.0927, 1.0927],\n",
       "        [1.0927, 0.3496, 0.0000, 0.0000, 0.0000, 0.7431, 1.0927, 1.0927],\n",
       "        [1.0927, 0.3496, 0.0000, 0.0000, 0.0000, 0.7431, 1.0927, 1.0927]],\n",
       "       grad_fn=<AddBackward0>)"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y(X)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "OrderedDict([('weight', tensor([[0.3496, 0.7431]])), ('bias', tensor([0.]))])"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y.state_dict()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "batch 2, loss 10.199\n",
      "batch 4, loss 3.415\n",
      "batch 6, loss 1.271\n",
      "batch 8, loss 0.499\n",
      "batch 10, loss 0.201\n",
      "batch 12, loss 0.082\n",
      "batch 14, loss 0.033\n",
      "batch 16, loss 0.014\n",
      "batch 18, loss 0.006\n",
      "batch 20, loss 0.002\n"
     ]
    }
   ],
   "source": [
    "# 构造一个二维卷积层，包含1个输出通道和形状为（1, 2）的卷积核\n",
    "conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)\n",
    "\n",
    "# 这个二维卷积层使用四维输入和输出格式，（卷积核个数、通道、高度、宽度）\n",
    "# 卷积核个数和通道数都为1\n",
    "X = X.reshape((1, 1, 6, 8))\n",
    "Y = Y.reshape((1, 1, 6, 7))\n",
    "\n",
    "for i in range(20):\n",
    "    Y_hat = conv2d(X)\n",
    "    l = (Y_hat - Y) ** 2\n",
    "    conv2d.zero_grad()\n",
    "    l.sum().backward()\n",
    "    # 迭代卷积核\n",
    "    conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad\n",
    "    if (i + 1) % 2 == 0:\n",
    "        print(f'batch {i+1}, loss {l.sum():.3f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[[ 0.9950, -1.0048]]]])"
      ]
     },
     "execution_count": 44,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "conv2d.weight.data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# exercise"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 构建⼀个具有对⻆线边缘的图像 X。\n",
    "    1. 如果将本节中举例的卷积核 K 应⽤于 X，会发⽣什么情况？\n",
    "    2. 如果转置 X 会发⽣什么？\n",
    "    3. 如果转置 K 会发⽣什么？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = torch.tensor([[1, 0, 0, 0, 0, 0, 0, 0],\n",
    "                  [0, 1, 0, 0, 0, 0, 0, 0],\n",
    "                  [0, 0, 1, 0, 0, 0, 0, 0],\n",
    "                  [0, 0, 0, 1, 0, 0, 0, 0],\n",
    "                  [0, 0, 0, 0, 1, 0, 0, 0],\n",
    "                  [0, 0, 0, 0, 0, 1, 0, 0],\n",
    "                  [0, 0, 0, 0, 0, 0, 1, 0],\n",
    "                  [0, 0, 0, 0, 0, 0, 0, 1]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 1.,  0.,  0.,  0.,  0.,  0.,  0.],\n",
       "        [-1.,  1.,  0.,  0.,  0.,  0.,  0.],\n",
       "        [ 0., -1.,  1.,  0.,  0.,  0.,  0.],\n",
       "        [ 0.,  0., -1.,  1.,  0.,  0.,  0.],\n",
       "        [ 0.,  0.,  0., -1.,  1.,  0.,  0.],\n",
       "        [ 0.,  0.,  0.,  0., -1.,  1.,  0.],\n",
       "        [ 0.,  0.,  0.,  0.,  0., -1.,  1.],\n",
       "        [ 0.,  0.,  0.,  0.,  0.,  0., -1.]])"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y2 = corr2d(X, k)\n",
    "y2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 1.,  0.,  0.,  0.,  0.,  0.,  0.],\n",
       "        [-1.,  1.,  0.,  0.,  0.,  0.,  0.],\n",
       "        [ 0., -1.,  1.,  0.,  0.,  0.,  0.],\n",
       "        [ 0.,  0., -1.,  1.,  0.,  0.,  0.],\n",
       "        [ 0.,  0.,  0., -1.,  1.,  0.,  0.],\n",
       "        [ 0.,  0.,  0.,  0., -1.,  1.,  0.],\n",
       "        [ 0.,  0.,  0.,  0.,  0., -1.,  1.],\n",
       "        [ 0.,  0.,  0.,  0.,  0.,  0., -1.]])"
      ]
     },
     "execution_count": 55,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "corr2d(X.t(), k)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 1., -1.,  0.,  0.,  0.,  0.,  0.,  0.],\n",
       "        [ 0.,  1., -1.,  0.,  0.,  0.,  0.,  0.],\n",
       "        [ 0.,  0.,  1., -1.,  0.,  0.,  0.,  0.],\n",
       "        [ 0.,  0.,  0.,  1., -1.,  0.,  0.,  0.],\n",
       "        [ 0.,  0.,  0.,  0.,  1., -1.,  0.,  0.],\n",
       "        [ 0.,  0.,  0.,  0.,  0.,  1., -1.,  0.],\n",
       "        [ 0.,  0.,  0.,  0.,  0.,  0.,  1., -1.]])"
      ]
     },
     "execution_count": 57,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "corr2d(X, k.t())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对角线边缘是一个对角矩阵，转置前后一样，核转置后有略微差异"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 在我们创建的 Conv2D ⾃动求导时，有什么错误消息？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[1., 1., 0., 0., 0., 0., 1., 1.],\n",
      "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
      "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
      "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
      "        [1., 1., 0., 0., 0., 0., 1., 1.],\n",
      "        [1., 1., 0., 0., 0., 0., 1., 1.]])\n",
      "tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
      "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
      "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
      "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
      "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],\n",
      "        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])\n",
      "torch.Size([6, 7])\n",
      "torch.Size([6, 7])\n",
      "batch 2, loss 13.209\n",
      "torch.Size([6, 7])\n",
      "torch.Size([6, 7])\n",
      "batch 4, loss 4.134\n",
      "torch.Size([6, 7])\n",
      "torch.Size([6, 7])\n",
      "batch 6, loss 1.479\n",
      "torch.Size([6, 7])\n",
      "torch.Size([6, 7])\n",
      "batch 8, loss 0.570\n",
      "torch.Size([6, 7])\n",
      "torch.Size([6, 7])\n",
      "batch 10, loss 0.227\n"
     ]
    }
   ],
   "source": [
    "X = torch.ones(6,8)\n",
    "X[:,2:6] = 0\n",
    "\n",
    "k = torch.tensor([[1, -1]])\n",
    "Y = corr2d(X, k)\n",
    "# X = X.reshape((1, 1, 6, 8))\n",
    "print(X)\n",
    "# Y = Y.reshape((1, 1, 6, 7))\n",
    "print(Y)\n",
    "\n",
    "self_conv2d = Conv2D((1, 2))\n",
    "for i in range(10):\n",
    "    Y_hat = self_conv2d(X)\n",
    "    print(Y_hat.shape)\n",
    "    l = (Y_hat - Y) ** 2\n",
    "    self_conv2d.zero_grad()\n",
    "    l.sum().backward()\n",
    "    # 迭代卷积核\n",
    "    self_conv2d.weight.data[:] -= 3e-2 * self_conv2d.weight.grad\n",
    "    if (i + 1) % 2 == 0:\n",
    "        print(f'batch {i+1}, loss {l.sum():.3f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 1.0368, -0.9393]])"
      ]
     },
     "execution_count": 67,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "self_conv2d.weight.data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([0.])"
      ]
     },
     "execution_count": 68,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "self_conv2d.bias.data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 如何通过改变输⼊张量和卷积核张量，将互相关运算表⽰为矩阵乘法？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 1, -1]])"
      ]
     },
     "execution_count": 70,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "torch.flip(k, [0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4.手工设计一些卷积核：\n",
    "\n",
    " - 二阶导数的核形式是什么？\n",
    " - 积分的核形式是什么？\n",
    " - 得到$d$次导数的最小核大小是多少？"
   ]
  },
  {
   "attachments": {
    "%E5%9B%BE%E7%89%87.png": {
     "image/png": "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uBr3w1XfirG1W7dAVfWNlfvnOlwnVvNg3g3Ceh1b3SbWbPrytIzKmzgrLze4uxkS5PcNzvJnLiTI39e/cqH7nASbf4bPSdAbcJ+NW6Cq161qP9EGWVUprbYdO7dAdRISbJRZbxkYbu96t0C1u0xS4UAVyDS3SNXeLck4SGf/Fqj640T8qnkyKJ6mDEY3htDOk2UQyWJIAMy3VetboNoJeWZXU61vMyusx3mtzsMYEaUwJBBRn2HnWQmOnricFcYDnal/Yr/2NKTdILfukkREPYMhgYj6lAMHDtgOCAcOHMhuYYy6AvGpsqcUhoQ0pBsSlHfvI6JexZBARH2OnSsKPXUFwbIrEPV7DAlpSCMkyF2FnD7sk4iygyGBiPqk48eP49ChQ8Jdj/bt24dDhw6ZjEEgIiKiTGBIICIiIiIiAUMCEREREREJGBKIiIiIiEjAkEBERERERAKGBCIiIiIiEjAkEBERERGRgCGBiIiIiIgEDAlERERERCRgSCAiIiIiIgFDAhERERERCRgSiIiIiIhIwJBAREREREQChgQiIiIiIhIwJBARERERkYAhgYiIiIiIBAwJREREREQkYEggIiIiIiIBQwIREREREQkYEoiIiIiISMCQQEREREREAoYEIiIiIiISMCQQEREREZGAIYGIiIiIiAQMCUREREREJGBIICIiIiIiAUMCEREREREJGBKIiIiIiEjAkEBERERERAKGBCIiIiIiEjAkEBERERGRgCGBiIiIiIgEDAlERERERCRgSCAiIiIiIgFDAhERERERCRgSiIiIiIhIwJBAREREREQChgQiIiIiIhIwJBARERERkYAhgYiIiIiIBAwJREREREQkYEggIiIiIiIBQwIREREREQkYEoiIiIiISMCQQEREREREAoYEIiIiIiISMCQQEREREZGAIYGIiIiIiAQMCUREREREJGBIICIiIuolHR0daG5uRjQaRSwWQzwex6FDh3q7WEQMCURERHTy2b9/P9avX4/a2lrEYjG0t7fj8OHDAICDBw9iw4YNiEajqKurw/r167F///5eLrHWjh07IEmS7t+OHTt6u3gnvY0bN6KiogKbN2/u7aKclBgSiIiI6KTy3nvvQZIkRKNRtLa2orGxEZIkoba2Fh0dHaipqYEkSWhoaEBraytisRgkScL777/f20VP2bx5sxAK6urqUF9f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"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "二阶导数的核形式可参考这个：\n",
    "https://dsp.stackexchange.com/questions/10605/kernels-to-compute-second-order-derivative-of-digital-image\n",
    "\n",
    "这里的导数应该是翻译的问题，借用讨论区的回答：\n",
    "![%E5%9B%BE%E7%89%87.png](attachment:%E5%9B%BE%E7%89%87.png)\n",
    "\n",
    "链接补充：https://en.wikipedia.org/wiki/Image_derivative"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
